package zaplog

import (
	"fmt"
	stdlog "log"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/spf13/viper"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"
)

const (
	LoggerKey = "zapLogger"
)

var (
	StrToLevel = map[string]zapcore.Level{
		"debug": zapcore.DebugLevel,
		"info":  zapcore.InfoLevel,
		"warn":  zapcore.WarnLevel,
		"error": zapcore.ErrorLevel,
	}

	Level *zap.AtomicLevel
)

var (
	levelToColor = map[zapcore.Level]Color{
		zapcore.DebugLevel:  Magenta,
		zapcore.InfoLevel:   Blue,
		zapcore.WarnLevel:   Yellow,
		zapcore.ErrorLevel:  Red,
		zapcore.DPanicLevel: Red,
		zapcore.PanicLevel:  Red,
		zapcore.FatalLevel:  Red,
	}

	unknownLevelColor = Red

	levelToLowercaseColorString = make(map[zapcore.Level]string, len(levelToColor))
	levelToCapitalColorString   = make(map[zapcore.Level]string, len(levelToColor))
)

type Log struct {
	Compress   bool
	RunMode    string
	Level      string
	Dir        string
	Encoding   string
	FileName   string
	MaxAge     int
	MaxSize    int
	MaxBackups int
	Handle     []string
}

func (l *Log) Validate() (ok bool, err error) {

	if _, ok := StrToLevel[strings.ToLower(l.Level)]; !ok {
		return false, fmt.Errorf("log.level is invalid: %s", l.Level)
	}

	if _, err := os.Stat(l.Dir); os.IsNotExist(err) {
		return false, fmt.Errorf("log.dir is invalid: %s", l.Dir)
	}

	if l.Encoding != "json" && l.Encoding != "console" {
		return false, fmt.Errorf("log.encoding is invalid: %s", l.Encoding)
	}

	if l.MaxAge <= 0 {
		return false, fmt.Errorf("log.max_age must larger than 0")
	}

	if l.MaxBackups <= 0 {
		return false, fmt.Errorf("log.max_backups must larger than 0")
	}

	return true, nil
}

type Logger struct {
	*zap.Logger
}

func NewLog(conf *viper.Viper) *Log {
	compress := conf.GetBool("log.compress")
	level := strings.TrimSpace(conf.GetString("log.level"))
	dir := strings.TrimSpace(conf.GetString("log.dir"))
	encoding := strings.TrimSpace(conf.GetString("log.encoding"))
	filename := strings.TrimSpace(conf.GetString("log.filename"))
	maxAge := conf.GetInt("log.max_age")
	maxSize := conf.GetInt("log.max_size")
	maxBackups := conf.GetInt("log.max_backups")
	handle := conf.GetStringSlice("log.handle")
	if level == "" {
		level = "info"
	}
	if dir == "" {
		dir = "log"
	}
	if filename == "" {
		filename = "running.log"
	}
	if encoding == "" {
		encoding = "text"
	}

	log := &Log{
		Compress:   compress,
		Level:      level,
		Dir:        dir,
		Encoding:   encoding,
		FileName:   filename,
		MaxAge:     maxAge,
		MaxSize:    maxSize,
		MaxBackups: maxBackups,
		Handle:     make([]string, 0),
	}

	for _, h := range handle {
		h = strings.TrimSpace(h)
		switch h {
		case "file", "console":
			log.Handle = append(log.Handle, h)
		default:
			stdlog.Printf("不支持的日志处理方式: %s", h)
		}
	}
	if len(log.Handle) == 0 {
		log.Handle = append(log.Handle, "console")
	}
	return log
}

func NewLogger(log *Log) *zap.Logger {
	file := filepath.Join(log.Dir, log.FileName)
	var level zap.AtomicLevel
	switch log.Level {
	case "debug":
		level = zap.NewAtomicLevelAt(zap.DebugLevel)
	case "info":
		level = zap.NewAtomicLevelAt(zap.InfoLevel)
	case "warn":
		level = zap.NewAtomicLevelAt(zap.WarnLevel)
	case "error":
		level = zap.NewAtomicLevelAt(zap.ErrorLevel)
	default:
		level = zap.NewAtomicLevelAt(zap.InfoLevel)
	}

	Level = &level

	var encoder zapcore.Encoder
	switch log.Encoding {
	case "json":
		encoder = zapcore.NewJSONEncoder(zapcore.EncoderConfig{
			TimeKey:        "ts",
			LevelKey:       "level",
			NameKey:        "logger",
			CallerKey:      "caller",
			MessageKey:     "msg",
			StacktraceKey:  "stacktrace",
			LineEnding:     zapcore.DefaultLineEnding,
			EncodeLevel:    zapcore.CapitalLevelEncoder,
			EncodeTime:     zapcore.EpochNanosTimeEncoder,
			EncodeDuration: zapcore.SecondsDurationEncoder,
			EncodeCaller:   zapcore.ShortCallerEncoder,
		})
	default:
		encoder = zapcore.NewConsoleEncoder(zapcore.EncoderConfig{
			TimeKey:          "ts",
			LevelKey:         "level",
			NameKey:          "logger",
			CallerKey:        "caller",
			MessageKey:       "msg",
			StacktraceKey:    "stacktrace",
			LineEnding:       zapcore.DefaultLineEnding,
			EncodeLevel:      levelEncoder,
			EncodeTime:       timeEncoder,
			EncodeDuration:   zapcore.SecondsDurationEncoder,
			EncodeCaller:     callerEncoder,
			ConsoleSeparator: " ",
		})
	}

	writer := make([]zapcore.WriteSyncer, 0, 1)
	for _, handle := range log.Handle {
		switch handle {
		case "file":
			writer = append(writer, zapcore.AddSync(&lumberjack.Logger{
				Filename:   file,           // Log file path
				MaxSize:    log.MaxSize,    // Maximum size unit for each log file: M
				MaxBackups: log.MaxBackups, // The maximum number of backups that can be saved for log files
				MaxAge:     log.MaxAge,     // Maximum number of days the file can be saved
				Compress:   log.Compress,   // Compression or not
			}))
		case "console":
			writer = append(writer, zapcore.AddSync(os.Stdout))
		}
	}

	core := zapcore.NewCore(
		encoder,
		zapcore.NewMultiWriteSyncer(writer...),
		Level,
	)

	if log.RunMode == "prod" {
		return zap.New(
			core,
			zap.Development(),
			zap.AddCaller(),
			zap.AddStacktrace(zap.PanicLevel),
		)
	}
	return zap.New(core, zap.AddCaller())
}

func timeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
	enc.AppendString("[" + t.Format(time.DateTime) + "]")
}

func levelEncoder(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
	s, ok := levelToCapitalColorString[level]
	if !ok {
		s = unknownLevelColor.Add(level.CapitalString())
	}
	enc.AppendString("[" + s + "]")
}

func callerEncoder(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) {
	enc.AppendString("[" + caller.TrimmedPath() + "]")
}

// NewContext Adds a field to the specified context
func (l *Logger) NewContext(ctx *gin.Context, fields ...zapcore.Field) {
	ctx.Set(LoggerKey, l.WithContext(ctx).With(fields...))
}

// WithContext Returns a zap instance from the specified context
func (l *Logger) WithContext(ctx *gin.Context) *Logger {
	if ctx == nil {
		return l
	}
	zl, _ := ctx.Get(LoggerKey)
	ctxLogger, ok := zl.(*zap.Logger)
	if ok {
		return &Logger{ctxLogger}
	}
	return l
}

func init() {
	for level, color := range levelToColor {
		levelToLowercaseColorString[level] = color.Add(level.String())
		levelToCapitalColorString[level] = color.Add(level.CapitalString())
	}
}
